Imports BitSmoke.Core.Data.Profiles
Imports BitSmoke.Core.Net.WorkQueue
Imports BitSmoke.Core.Net.Messages

Imports System.Net.Sockets

Namespace Core.Net.Connection
    Public Enum ConnectionStateConstants
        ConnectionState_Disconnected
        ConnectionState_Connecting
        ConnectionState_Connected
    End Enum

    Public Class DataConnection
        Implements IDisposable

#Region " Public Constants "
        Public Const ProtocolVersion As Byte = 0
#End Region

#Region " Private Constants "
        Private Const ReadBufferSize As Integer = 1024
#End Region

#Region " Private Properties "
        Private zTCP As TcpClient
        Private zUP As UserProfile
        Private zState As ConnectionStateConstants ' only ever set using setState to ensure the event gets raised
        Private zBA_ReadBuffer As Byte() = New Byte(ReadBufferSize) {}
        Private zI_BytesInReadBuffer As Integer
        Private WithEvents zBWQ As New BackGroundWorkQueue(Me)
#End Region

#Region " Public Properties "
        Public ReadOnly Property State() As ConnectionStateConstants
            Get
                Return zState
            End Get
        End Property
#End Region

#Region " Events "
        Public Event StateChange(ByVal pState As ConnectionStateConstants)
#End Region

#Region " Constructors "
        Public Sub New(ByVal pUP As UserProfile)
            zUP = pUP
            zTCP = New TcpClient

            setState(ConnectionStateConstants.ConnectionState_Connecting)

            zTCP.BeginConnect(zUP.IP, zUP.Port, New AsyncCallback(AddressOf zTCP_ConnectCallback), zTCP)
        End Sub

        Public Sub New(ByVal pUP As UserProfile, ByVal pTCP As TcpClient)
            zUP = pUP
            zTCP = pTCP

            'TODO: do we need to do reads on a timer for bandwidth limit prefs?
            zTCP.GetStream.BeginRead(zBA_ReadBuffer, zI_BytesInReadBuffer, ReadBufferSize, New AsyncCallback(AddressOf zTCP_ReadCallback), zTCP.GetStream)

            setState(ConnectionStateConstants.ConnectionState_Connected)
        End Sub
#End Region

#Region " Private Methods "
        Private Sub setState(ByVal pState As ConnectionStateConstants)
            If pState <> zState Then
                zState = pState

                RaiseEvent StateChange(zState)
            End If
        End Sub
#End Region

#Region " Callbacks "
        Private Sub zTCP_ConnectCallback(ByVal ar As IAsyncResult)
            'TODO: if the connection fails does it still call this method with that info in the aresult?
            Dim tcp As TcpClient = CType(ar.AsyncState, TcpClient)
            tcp.EndConnect(ar)

            'TODO: do we need to do reads on a timer for bandwidth limit prefs?
            zTCP.GetStream.BeginRead(zBA_ReadBuffer, zI_BytesInReadBuffer, ReadBufferSize, New AsyncCallback(AddressOf zTCP_ReadCallback), zTCP.GetStream)

            setState(ConnectionStateConstants.ConnectionState_Connected)

            Debug.WriteLine("connection established!")
        End Sub

        Private Sub zTCP_ReadCallback(ByVal ar As IAsyncResult)
            Dim ns As NetworkStream = CType(ar.AsyncState, NetworkStream)
            Dim iBytesRead As Integer

            Try
                iBytesRead = ns.EndRead(ar)
            Catch e As SocketException
                Debug.WriteLine("EX: connection died, possibly we are shutting down")
                Return
            Catch e As IO.IOException
                Debug.WriteLine("EX: connection died, peer probably went away")
                Return
            End Try

            zI_BytesInReadBuffer += iBytesRead

            'TODO: update statistics with iBytesRead

            Dim iMsgSize As UInt16
            Dim iOffset As Integer
            Dim baMsg As Byte()
            While True
                iMsgSize = BitConverter.ToUInt16(zBA_ReadBuffer, iOffset)

                If iOffset + iMsgSize <= zI_BytesInReadBuffer Then 'there is a complete message in the buffer
                    baMsg = New Byte(iMsgSize) {}
                    Array.ConstrainedCopy(zBA_ReadBuffer, iOffset, baMsg, 0, iMsgSize)
                    iOffset += iMsgSize

                    'add message to parse queue
                    zBWQ.addWork(New WI_ParseMessage(baMsg))
                Else
                    Exit While
                End If
            End While

            ' remove processed messages and make sure the buffer has ReadBufferSize space at the end
            'TODO: is there a more eficient way of doing this than creating a new array?
            zI_BytesInReadBuffer -= iOffset
            Dim baNewBuffer As Byte() = New Byte(zI_BytesInReadBuffer + ReadBufferSize) {}
            Array.ConstrainedCopy(zBA_ReadBuffer, iOffset, baNewBuffer, 0, zI_BytesInReadBuffer)
            zBA_ReadBuffer = baNewBuffer

            ns.BeginRead(zBA_ReadBuffer, zI_BytesInReadBuffer, ReadBufferSize, New AsyncCallback(AddressOf zTCP_ReadCallback), ns)
        End Sub

        Private Sub zBWQ_WorkCompleted(ByVal pWI As IWorkItem, ByVal pObj_Result As Object) Handles zBWQ.WorkCompleted
            Dim msg As Message = CType(pObj_Result, Message)
            'TODO: do something with the message
        End Sub
#End Region

#Region " IDisposable Support "
        Private disposedValue As Boolean = False        ' To detect redundant calls

        ' IDisposable
        Protected Overridable Sub Dispose(ByVal disposing As Boolean)
            If Not Me.disposedValue Then
                If disposing Then
                    ' free unmanaged resources when explicitly called
                    zBWQ.Dispose()
                    zBWQ = Nothing

                    zTCP = Nothing
                End If

                ' free shared unmanaged resources
            End If
            Me.disposedValue = True
        End Sub

        ' This code added by Visual Basic to correctly implement the disposable pattern.
        Public Sub Dispose() Implements IDisposable.Dispose
            ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
#End Region
    End Class
End Namespace
